package hydrap2p.proxy.manager;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.apache.thrift.TException;

import hydrap2p.ServiceLocation;
import hydrap2p.logging.Level;

/**
 * Keep track of what local ports receive traffic from remote ports
 * (Local port gets opened, then asks for a callback port)
 * @author Hufman
 *
 */
public class CallbackManager {
	private static CallbackManager instance;
	
	private Map<ServiceLocation, ServiceLocation> callbacks;
	private Map<Integer, List<ServiceLocation>> peermaps;
	private List<ServiceLocation> connecting;	// callbacks waiting to connect
	
	private CallbackManager()
	{
		callbacks=new TreeMap<ServiceLocation,ServiceLocation>();
		connecting=new LinkedList<ServiceLocation>();
		peermaps=new TreeMap<Integer, List<ServiceLocation>>();
	}
	
	public static CallbackManager getInstance()
	{
		if (instance==null)
		{
			synchronized(CallbackManager.class)
			{
				if (instance==null)
					instance=new CallbackManager();
			}
		}
		return instance;
	}
	
	/**
	 * Given a local ServiceLocation, create a remote callback port to the server where the target is, and return the remote directory
	 * @param local
	 * @return
	 * @throws TException 
	 */
	public ServiceLocation openCallbackPort(ServiceLocation local, ServiceLocation target, int peerid) throws TException
	{
		ProxyLogging.log(Level.DEBUG, "ProxyManager - CallbackManager", "Opening callback request: local="+local+" target="+target+" peerid="+peerid);
		synchronized(callbacks)
		{
			if (callbacks.containsKey(local))
				return callbacks.get(local);
		}
		
		// check that it's not already connecting
		boolean myConnecting=false;
		boolean connected=false;
		synchronized(connecting)
		{
			if (!connecting.contains(local))
			{
				connecting.add(local);
				myConnecting=true;
			}
		}
		
		// actually connect
		if (myConnecting)
		{
			//ProxyLogging.log(Level.DEBUG, "ProxyManager - CallbackManager", "Starting to connect");

			if (target!=null)
				peerid=ServiceTunnels.getInstance().findPeerID(target.port);
			
			ServiceLocation ret;
			if (target==null)
				ret=ManagerConnectionManager.getInstance().openCallbackPort(local, peerid);
			else
				ret=ManagerConnectionManager.getInstance().openCallbackPort(local, target);
			synchronized(callbacks)
			{
				callbacks.put(local, ret);
			}
			synchronized(peermaps)
			{
				if (!peermaps.containsKey(peerid))
					peermaps.put(peerid, new LinkedList<ServiceLocation>());
				peermaps.get(peerid).add(local);
			}
			synchronized(connecting)
			{
				connecting.remove(local);
				connecting.notifyAll();
			}
		}
		else	// wait for the other thread to connect
		{
			//ProxyLogging.log(Level.DEBUG, "ProxyManager - CallbackManager", "Waiting for other thread to connect");

			while (!connected)
			{
				synchronized(connecting)
				{
					try {
						connecting.wait();
					} catch (InterruptedException e) {
						// um, someone wants us to die, i think
					}
					if (!connecting.contains(local))	// it finished connecting!
						connected=true;
					// else, we're still connecting, so keep waiting
				}
			}
		}

		// at this point, if there was a successful connection, it should exist in callbacks
		synchronized(callbacks)
		{
			//ProxyLogging.log(Level.DEBUG, "ProxyManager - CallbackManager", "Successful callback established");

			if (callbacks.containsKey(local))
				return callbacks.get(local);
		}
		ProxyLogging.log(Level.ERROR, "ProxyManager - CallbackManager", "Failed to connect callback port");
		throw new TException("Could not establish callback");
	}
	
	public void closePeerCallbacks(int peerid)
	{
		List<ServiceLocation> temp;
		synchronized(peermaps)
		{
			temp=peermaps.get(peerid);
			peermaps.remove(peerid);
		}
		if (temp!=null)
		{
			for (ServiceLocation temp2:temp)
			{
				closeCallback(temp2);
			}
		}
	}
	
	/**
	 * Forget about the callback to this service location
	 * @param local
	 */
	public void closeCallback(ServiceLocation local)
	{
		// TODO
		callbacks.remove(local);
	}
	
	public ServiceLocation getRemotePort(ServiceLocation local)
	{
		
		synchronized(callbacks)
		{
			return callbacks.get(local);
		}
	}
	/**
	 * Given a service location from the Other Side, find the listening port at the Local Side to match
	 * @param remote
	 * @return
	 */
	public ServiceLocation getLocalPort(ServiceLocation remote)
	{
		synchronized(callbacks)
		{
			for (Map.Entry<ServiceLocation, ServiceLocation> pair : callbacks.entrySet())
			{
				if (pair.getValue().equals(remote))
					return pair.getKey();
			}
		}
		return null;
	}
}
